function defineReactive(obj, key, val) {
    observe(val)

    const dep = new Dep();
    Object.defineProperty(obj, key, {
        get() {
            console.log(`get ${key}, ${val}`);

            Dep.target && dep.addDep(Dep.target)
            return val;
        },
        set(newVal) {
            if (val !== newVal) {
                console.log(`set ${key}, ${newVal}`);
                observe(newVal)
                val = newVal;

                // watchers.forEach(w => w.update())
                dep.notify()
            }
        }
    })
}

function observe(obj) {
    if (typeof obj !== 'object' || obj == null) {
        return
    } 

    new Observer(obj)
}

class Observer {
    constructor(value) {
        if (Array.isArray(value)) {
            // todo
        } else {
            this.walk(value)
        }
    }

    walk(obj) {
        Object.keys(obj).forEach(key => {
            defineReactive(obj, key, obj[key])
        })
    }
}

function set(obj, key, val) {
    defineReactive(obj, key, val)
}

// const obj = {}
// observe(obj)
// obj.foo = "fooolll"
// obj.foo
// console.log("demo done")

function proxy(vm) {
    Object.keys(vm.$data).forEach(key => {
        Object.defineProperty(vm, key, {
            get() {
                return vm.$data[key]
            },
            set(v) {
                vm.$data[key] = v
            }
        })
    })
}

class KVue {
    constructor(options) {
        this.$options = options
        this.$data = options.data

        // make the data reactive
        observe(this.$data)

        proxy(this)

        new Compile(options.el, this)
    }
}


class Compile {
    constructor(el, vm) {
        this.$vm = vm
        this.$el = document.querySelector(el)

        if (this.$el) {
            this.compile(this.$el)
        }
    }

    compile(el) {
        const childNodes = el.childNodes

        childNodes.forEach(node => {
            if (node.nodeType === 1) {
                // element
                const attrs = node.attributes
                Array.from(attrs).forEach(attr => {
                    const attrName = attr.name
                    const exp = attr.value
                    if (attrName.startsWith("k-")) {
                        const dir = attrName.substring(2)
                        this[dir] && this[dir](node, exp)
                    }else if (attrName.startsWith("@")) {
                        const dir = attrName.substring(1)
                        this[dir] && this[dir](node, exp)
                    }else if (attrName === "v-model") {
                        const dir = "input_model"
                        this[dir] && this[dir](node, exp)
                    }

                })
            } else if (this.isInter(node)) {
                // text
                console.log("inter", node.textContent)
                this.compileText(node)
            }

            if (node.childNodes && node.childNodes.length > 0) {
                this.compile(node)
            }
        })
    }

    isInter(node) {
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
    }

    compileText(node) {
        // node.textContent = this.$vm[RegExp.$1]
        this.update(node, RegExp.$1, 'text')
    }

    update(node, exp, directive) {
        // 1. init 
        const fn = this[directive + 'Updater']
        fn && fn(node, this.$vm[exp]) 
        
        // 2. update
        new Watcher(this.$vm, exp, function(val) {
            fn && fn(node, val)
        })
    }

    text(node, exp) {
        this.update(node, exp, 'text')
    }

    textUpdater(node, value) {
        node.textContent = value
    }

    click(node, exp) {
        console.log("click is called. to run exp");
        node.addEventListener('click', this.$vm.$options[exp])
    }

    input_model(node, exp) {
        const newVal = this.$vm.$data[exp]
        node.value = newVal
        set(this.$vm.$data, 'input_data', newVal)
        const vm = this.$vm
        node.addEventListener('keyup', function(e) {
            console.log(e)
            console.log("data has been changed")
            vm.$data[exp] = e.target.value
        }, false)
    }

}


const watchers = []

class Watcher {
    constructor(vm, key, updateFn) {
        this.vm = vm;
        this.key = key;
        this.updateFn = updateFn;

        // watchers.push(this);
        Dep.target = this
        this.vm[this.key]
        Dep.target = null
    }

    update() {
        this.updateFn.call(this.vm, this.vm[this.key])
    }
}


class Dep {
    constructor() {
        this.deps = []
    }

    addDep(dep) {
        this.deps.push(dep)
    }

    notify() {
        this.deps.forEach(dep => dep.update())
    }
}